-
Notifications
You must be signed in to change notification settings - Fork 821
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
XMLHttpRequest #595
XMLHttpRequest #595
Conversation
Codecov Report
@@ Coverage Diff @@
## master #595 +/- ##
==========================================
- Coverage 90.48% 90.23% -0.25%
==========================================
Files 180 189 +9
Lines 9109 9608 +499
Branches 814 877 +63
==========================================
+ Hits 8242 8670 +428
- Misses 867 938 +71
|
/cc @draffensperger |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some first pass comments
protected patch() { | ||
this._logger.debug('applying patch to', this.moduleName, this.version); | ||
|
||
if (isWrapped(XMLHttpRequest.prototype.open)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would this already be patched? Shouldn't we just no-op if it is already wrapped?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
with web there is always some probability that something can be destroyed and recreated again. This way it will always work correctly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is patching a global, would it not patch if multiple components on the same page are instrumented with separate tracers/configs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
copy paste here my reply from below
We are using shimmer which doesn't support multiple wrap of the same function so if you even register multiple tracers with the same plugins, and try to unpatch just one, it will unpatch the global successfully anyway. So if the decision to use shimmer was done my assumption is that we are able to instrument using the plugins once only. Otherwise the code will be broken anyway.
packages/opentelemetry-core/src/trace/instrumentation/basePluginUtils.ts
Outdated
Show resolved
Hide resolved
): OpenFunction | any { | ||
plugin._createSpan(this as XMLHttpRequestWrapped, url, method); | ||
|
||
if (async) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it is always safer to use original.apply(this, arguments)
because we can't account for users passing incorrect values or the incorrect number of values. It also removes the need for this check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is the same here.
You don't have arguments
word in typescript , so you would still declare array of arguments and define type for it, which would be quite weird anyway.
Besides that check also the whole code to see more context. The async will always be set to true
and there are reasons for that check here. It is also worth to mention that it was ported from open census.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You do have the arguments
keyword in typescript.
It is possible to call open with async: false
and user not-null. In this case you are changing the arguments the user has called the function with.
We cannot assume that users will use the API properly as documented and it is much easier and safer to directly pass their arguments. If the user incorrectly calls open
with async: true
I am suggesting something like this (tests pass with this version):
protected _patchOpen() {
return (original: Function): OpenFunction => {
const plugin = this;
return function patchOpen(
this: XMLHttpRequest,
method: string,
url: string,
async?: boolean,
user?: string | null,
pass?: string | null
): ReturnType<OpenFunction> {
plugin._createSpan(this as XMLHttpRequestWrapped, url, method);
return original.apply(this, arguments);
/*
If you would rather use `call` this can be done easily
return original.call(this, ...arguments);
*/
};
};
}
Regarding the performance difference between apply and call, it may be the case that call
is faster than apply
, but in this case the if
statement would provide more overhead than the difference.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
still cannot use apply anyway, as I mentioned previously please read the spec, and why it needs to force the async
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If true, notification of a completed transaction is provided using event listeners.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about that means it must be true? If it is false (synchronous), then you just finish the span after calling the original function. If user sets async false in a context that it is not allowed (some browsers only allow sync requests in workers), then
The way your code is currently, it changes the behavior of user code behind the scenes. If I am a user and I make a synchronous xhr request, I expect it to be made synchronously not transparently rewritten to be async behind the scenes by my telemetry provider.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://github.com/census-instrumentation/opencensus-web/blob/68aa988732e03f2de49ebc0213c3b2525aa5b68b/packages/opencensus-web-instrumentation-zone-peer-dep/src/monkey-patching.ts#L31
and
https://github.com/census-instrumentation/opencensus-web/blob/68aa988732e03f2de49ebc0213c3b2525aa5b68b/packages/opencensus-web-instrumentation-zone-peer-dep/src/monkey-patching.ts#L54
and
https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Synchronous_and_Asynchronous_Requests
Can you guys @draffensperger @mayurkale22 have your input here too, thx :).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and one more Synchronous XHR is now in deprecation state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand that it's deprecated, but that doesn't mean users can't or won't use it and the telemetry library shouldn't silently change the behavior of the program.
should we tallk about this in the SIG tomorrow?
}; | ||
plugin._addHeaders(this, currentSpan); | ||
} | ||
return original.call(this, body); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return original.call(this, body); | |
return original.apply(this, arguments); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- call is faster
- we already know there is just one para, we know it's type so we can use it and typescript will also validate the type of this is correcty
- You don't have
arguments
word in typescript , so you would still declare array of aguments and define type for it, which would be quite weird anyway
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and the same here, thx
@draffensperger @mayurkale22 ^^
packages/opentelemetry-core/src/trace/instrumentation/basePluginUtils.ts
Outdated
Show resolved
Hide resolved
packages/opentelemetry-core/src/trace/instrumentation/basePluginUtils.ts
Outdated
Show resolved
Hide resolved
packages/opentelemetry-plugin-xml-http-request/src/enums/AttributeNames.ts
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work!
packages/opentelemetry-plugin-xml-http-request/src/enums/AttributeNames.ts
Show resolved
Hide resolved
marking temporary as WIP - working on better detection for cors |
*/ | ||
export class XMLHttpRequestPlugin extends BasePlugin<XMLHttpRequest> { | ||
readonly component: string = 'xml-http-request'; | ||
readonly version: string = '0.3.0'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would assume that the user has the same version of the plugin and of core installed which may not always be the case. @obecny and I had talked about adding the autogenerated VERSION file to all lerna packages, but nothing has been done yet.
): OpenFunction | any { | ||
plugin._createSpan(this as XMLHttpRequestWrapped, url, method); | ||
|
||
if (async) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You do have the arguments
keyword in typescript.
It is possible to call open with async: false
and user not-null. In this case you are changing the arguments the user has called the function with.
We cannot assume that users will use the API properly as documented and it is much easier and safer to directly pass their arguments. If the user incorrectly calls open
with async: true
I am suggesting something like this (tests pass with this version):
protected _patchOpen() {
return (original: Function): OpenFunction => {
const plugin = this;
return function patchOpen(
this: XMLHttpRequest,
method: string,
url: string,
async?: boolean,
user?: string | null,
pass?: string | null
): ReturnType<OpenFunction> {
plugin._createSpan(this as XMLHttpRequestWrapped, url, method);
return original.apply(this, arguments);
/*
If you would rather use `call` this can be done easily
return original.call(this, ...arguments);
*/
};
};
}
Regarding the performance difference between apply and call, it may be the case that call
is faster than apply
, but in this case the if
statement would provide more overhead than the difference.
You may also want to do the same with the other maps that have possibility of leaks:
which then removes the need for |
@dyladan not all can be done and taskCount is still needed, but all things that depends on xhr has been moved. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💯 👍 Great job on this.
I'm giving approval since to me this semantically captures what we should be doing in this plugin. I left a couple more minor comments/suggestions and in particular I think the issue around the cast vs. use of the span API is relevant to fix if we can. But this is great!!
As an aside, I saw the comment thread about this assuming there is a trace context set up, which to me is a relevant comment, and highlights the need for this plugin to work in conjunction with other plugins that set up a "root" span of sorts, e.g. a user interaction tracing plugin, a root span from document load, etc.
} | ||
} | ||
if (!addHeaderOnDifferentOrigin) { | ||
this._logger.warn('Cannot set headers on different origin'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this will be a pretty routine case that the user sends an XHR to a different origin that they don't want to pass along trace headers for (that is, at least until the browser spec can give an exception for CORS for the W3C trace headers)
Given that, would it make sense to remove this warning log and corresponding if
block?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might be wrong but my understanding is that we might break something with the request if we force to add headers. The headers are only added when origin is the same. If not then I check if user allows to add the headers based on config. If not then the headers are dropped and warning is being show. I cannot remove the if block (to prevent adding headers) but only warning if that is something you want me to do ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks real good!
From my perspective I really only have one outstanding question about forcing all calls to open
to be async even when the user passes false. I'd like to get @draffensperger and @mayurkale22 opinion on this.
It looks like the dependency on web
in this version is limited to only functions that don't require casts to our sdk types so they should be compatible even with an sdk other than ours being used. 👍
): void { | ||
plugin._createSpan(this, url, method); | ||
|
||
return original.call(this, method, url, true, user, pass); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Creating a new thread on this line as the old one is deep in the history and on a line that has been outdated for a long time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think I missed this in my review, but I definitely agree that we should preserve the original behavior of the XHR function including the async parameter.
const onClick = () => { | ||
for (let i = 0, j = 5; i < j; i++) { | ||
const span1 = webTracerWithZone.startSpan(`files-series-info-${i}`, { | ||
parent: webTracerWithZone.getCurrentSpan() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe if there is no current span, this span will just be a root span, which is the correct behavior. I think my comment about having a nice root span for XHRs is more like a future feature and not so much an issue with this code.
@draffensperger @mayurkale22 @dyladan changed the code with regards to our meeting |
@obecny just wanted to confirm, do you have any outstanding work here or PR is ready to merge? |
Nothing left from my side |
Which problem is this PR solving?
Short description of the changes
It adds auto instrumentation for XMLHttpRequest including trace headers